Skip to content

feat(core): add NIP-05 over Namecoin (.bit) resolver#391

Draft
mstrofnone wants to merge 2 commits into
nostr-dev-kit:masterfrom
mstrofnone:feat/nip05-namecoin
Draft

feat(core): add NIP-05 over Namecoin (.bit) resolver#391
mstrofnone wants to merge 2 commits into
nostr-dev-kit:masterfrom
mstrofnone:feat/nip05-namecoin

Conversation

@mstrofnone
Copy link
Copy Markdown

@mstrofnone mstrofnone commented May 21, 2026

Summary

Adds a NIP-05 over Namecoin (.bit) resolver to @nostr-dev-kit/ndk as
a sibling to the existing DNS-based getNip05For path. Identifiers like
alice@example.bit, example.bit, d/example, and id/alice can be
resolved against the Namecoin blockchain via a caller-supplied ElectrumX
transport.

The resolver also honours the
ifa-0001
§"import" mechanism, so records that delegate shared blocks into a
sibling name (e.g. the canonical demo target testls.bit, whose apex
record is up against the 520-byte per-name limit and delegates its
nostr.names block to dd/testls) resolve correctly without the
caller having to know about the indirection. Non-import records pay
zero extra I/O.

Tracking the in-flight NIP draft and its other implementations:

The parser semantics (local-part priority: exact → _ → first valid;
d/ vs id/ namespacing; tolerated leading nostr: prefix; simple
"nostr": "hex" vs extended "nostr": { names, relays, nip46 } JSON
shapes) match those references byte-for-byte.

Design choices

  • Zero new dependencies. Uses the existing @noble/hashes/sha2.js
    for the ElectrumX scripthash. No nostr-tools-style additions.
  • Transport-free core. NDK targets browsers, React Native, and Node;
    bundling a WSS client is undesirable. The parsing module exposes
    buildNameIndexScript, electrumScriptHash, and
    parseNameUpdateScript so callers can drive any ElectrumX transport
    they like. getNamecoinNip05For accepts an injected resolver
    function and otherwise reads ndk.namecoinResolver.
  • Isomorphic. No node: imports, no import 'ws', no Node-only
    globals. TextEncoder is available on every NDK target; a manual
    encoder is provided as a fallback.
  • No regressions to the DNS path. NDKUser.fromNip05 only routes
    identifiers through Namecoin when (a) the identifier matches the
    Namecoin predicate and (b) a resolver is configured. Every other
    identifier still flows through getNip05For unchanged. nip05.ts
    is untouched.

ifa-0001 "import" support

The second commit on this branch (6c4e2847) adds
expandImports, a recursive merger that handles every shape of the
spec:

  • Four import value shapes — bare string, single-array,
    pair-array, canonical array-of-arrays — all normalise to the
    canonical form.
  • Subdomain-selector walk via the imported value's map tree
    (exact label > * wildcard > empty default, DNS-rightmost-first).
  • Importer-wins shallow merge with null as a delete marker
    (semantic suppression per ifa-0001).
  • Recursion up to a configurable depth (default 4, the spec minimum);
    deeper chains are silently truncated and the importer's own fields
    are retained.
  • Cycle protection via a visited (name|selector) set scoped to one
    top-level expansion.
  • Lenient I/O — lookup null, throws, or malformed JSON resolve to
    {} so transient ElectrumX hiccups don't kill resolution.
  • The import key is stripped from the merged result.

The expansion runs once between parsing the apex value and extracting
the nostr field, so the existing extractor sees a richer merged
object without any further changes. Non-import records short-circuit
before any lookup and cost zero extra I/O.

New public symbols

From @nostr-dev-kit/ndk:

  • isValidNamecoinIdentifier / isDotBit
  • NamecoinAddress (with parse, toString, electrumScriptHash)
  • extractNostrFromValue, profilePointerFromRawJson
  • buildNameIndexScript, electrumScriptHash, parseNameUpdateScript
  • DEFAULT_ELECTRUMX_SERVERS
  • getNamecoinNip05For, getNamecoinNip05User
  • expandImports, DEFAULT_IMPORT_MAX_DEPTH
  • Types: NamecoinResolver, GetNamecoinNip05Opts,
    NamecoinNip05Extract, ElectrumXServer,
    NamecoinImportLookup, NamecoinValue,
    NIP05_NAMECOIN_REGEX_BIT, NIP05_NAMECOIN_REGEX_NAMESPACED

On the NDK class:

  • ndk.namecoinResolver?: NamecoinResolver — optional. When set,
    NDKUser.fromNip05("alice@example.bit", ndk) returns the Namecoin
    result.

Caller-side resolver shape

import {
    NamecoinAddress,
    parseNameUpdateScript,
} from "@nostr-dev-kit/ndk";

ndk.namecoinResolver = async (address) => {
    // 1. Open WSS to one of DEFAULT_ELECTRUMX_SERVERS.
    // 2. Call blockchain.scripthash.get_history with address.electrumScriptHash().
    // 3. Fetch the most recent transaction with blockchain.transaction.get.
    // 4. Decode the NAME_UPDATE output with parseNameUpdateScript and
    //    return new TextDecoder().decode(value).
    return rawJson;
};

The transport itself is intentionally left out of the npm package: it
varies per consumer (browser, React Native, Tauri/Electron) and the
ElectrumX server certificates are self-signed.

The same callback is reused for any import targets the apex value
declares — no extra wiring required on the caller side.

Tests

  • core/src/user/nip05namecoin.test.ts: 34 vitest cases covering
    identifier validation, parser semantics, simple + extended JSON
    shapes (including nip46 extraction), script encode/decode
    round-trips, ElectrumX scripthash math, OP_PUSHDATA1 framing, and
    the resolver end-to-end with a mock transport.
  • core/src/user/nip05namecoin-import.test.ts: 21 new vitest cases
    covering every import behaviour from ifa-0001 — all four value
    shapes, selector walks (DNS order, wildcards), importer-wins merge
    with null suppression, 4-level recursion, depth-budget
    truncation, lookup failures (null / throw / malformed JSON),
    malformed import values, cycle breaking, and the end-to-end
    testls.bit import pattern through a fake ElectrumX transport.
  • bun vitest run src/user/nip05namecoin.test.ts src/user/nip05namecoin-import.test.ts
    from core/: 55 passed, 0 failed.
  • Full core suite: same pass/fail counts as master — no
    regressions from this PR (the unrelated pre-existing failures in
    auth-retry, connectivity, fetchEvent-guardrails,
    ai-guardrails, and user/index exist identically on master).

Marked as draft while the NIP draft and sibling implementations
land.

Adds a transport-free parsing module plus an NDK-integrated resolver for
NIP-05 identifiers rooted in the Namecoin blockchain (`.bit`).

- core/src/user/nip05namecoin.ts: identifier validation, NamecoinAddress
  parser, JSON extraction matching the Kotlin/Swift/Go/Rust references
  byte-for-byte (local-part priority: exact -> "_" -> first valid),
  ElectrumX scripthash helpers, and OP_NAME_UPDATE script encode/decode.
- core/src/user/nip05namecoin-resolver.ts: getNamecoinNip05For honors
  ndk.queuesNip05 and ndk.cacheAdapter the same way getNip05For does.
  Transport is injected by the caller; NDK stays isomorphic.
- core/src/ndk/index.ts: optional ndk.namecoinResolver field.
- core/src/user/index.ts: NDKUser.fromNip05 routes .bit/d/id identifiers
  through the Namecoin path when a resolver is configured; the DNS path
  is preserved for every other identifier.
- core/src/index.ts: re-exports the public surface.

Adds 34 vitest cases covering identifier validation, parser semantics,
simple+extended JSON shapes, script encode/decode round-trips,
ElectrumX scripthash math, and the resolver end-to-end with a mock.

No new runtime dependencies; uses the existing @noble/hashes/sha2.js.

Upstream NIP PR: nostr-protocol/nips#2349
Companions: nostr-tools#533, rust-nostr/nostr#1367.
Namecoin's 520-byte per-name limit makes apex records crowded.
ifa-0001 §"import" lets a record delegate shared blocks into a sibling
name via an `"import"` key on the JSON value. Without import-chain
handling, NIP-05 lookups against records that use this pattern (the
canonical demo target `testls.bit` is one) silently fail: the resolver
sees the apex value, finds no `nostr` field, and returns null — never
consulting the imported sibling that actually carries the
`nostr.names` block.

This commit adds `expandImports`, a recursive merger that honors every
shape of the spec:

  - Four `import` value shapes (bare string, single-array,
    pair-array, canonical array-of-arrays).
  - Subdomain-selector walk via the imported value's `map` tree
    (exact label > `*` wildcard > empty default, DNS-rightmost-first).
  - Importer-wins shallow merge with `null` as a delete marker.
  - Recursion up to a configurable depth (default 4, the spec
    minimum); deeper chains truncated, importer's own fields retained.
  - Cycle protection via a visited `(name|selector)` set.
  - Lenient I/O: lookup nulls, throws, or malformed JSON resolve to
    `{}` so transient ElectrumX hiccups don't kill resolution.
  - `import` key stripped from the final merged result.

`getNamecoinNip05For` calls `expandImports` between parsing the apex
value and extracting the `nostr` field. Non-import records cost zero
extra I/O (the trigger short-circuits before any lookup).

21 new vitest cases cover every behaviour above, including the
`testls.bit` real-world pattern end-to-end through a fake ElectrumX
transport.
@mstrofnone mstrofnone force-pushed the feat/nip05-namecoin branch from 6c4e284 to ad4e45a Compare May 23, 2026 20:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant